package fr.labri.macchiato.framework.business.service;

import java.util.List;
import java.util.Map;

import fr.labri.macchiato.framework.business.finder.Callback;
import fr.labri.macchiato.framework.business.finder.ProductFinder;
import fr.labri.macchiato.framework.business.model.cart.Cart;
import fr.labri.macchiato.framework.business.model.preferences.UserPreferences;
import fr.labri.macchiato.framework.business.model.product.Article;
import fr.labri.macchiato.framework.business.model.product.Model;
import fr.labri.macchiato.framework.business.model.product.Offer;
import fr.labri.macchiato.framework.business.model.product.Product;
import fr.labri.macchiato.framework.business.model.project.Project;
import fr.labri.macchiato.framework.business.model.project.ProjectFactory;
import fr.labri.macchiato.framework.technical.repository.Repository;
import fr.labri.macchiato.framework.technical.repository.TransientRepository;
import fr.labri.macchiato.framework.util.FrameworkException;

/**
 * Service for the project class. <br>
 * All accesses to a {@link Project} instance should be done through this class,
 * as it deals with when to save a project into the {@link Repository}, and
 * ensures the identity of projects (the name of a project must be unique)
 * 
 * <p>
 * In order to keep the observers working, we always use a
 * {@link TransientRepository} to store the projects during runtime, as
 * observers are transient and cannot be saved in the browser's cache
 * </p>
 * 
 * @author Matthieu FOUCAULT
 * 
 */
public class ProjectService {

	private PreferencesService preferencesService;
	private ProductFinder productFinder;
	private Repository<Project, String> projectRepository;

	// this is the repository used to keep the observer working
	private TransientRepository<Project, String> projectTransientRepository;

	/**
	 * Creates a new {@link ProjectService}
	 * 
	 * @param projectRepository
	 * @param preferencesService
	 * @param productFinder
	 */
	public ProjectService(Repository<Project, String> projectRepository,
			PreferencesService preferencesService, ProductFinder productFinder) {

		this.productFinder = productFinder;
		this.preferencesService = preferencesService;
		this.projectRepository = projectRepository;
		this.projectTransientRepository = new TransientRepository<Project, String>();

		List<Project> projectList = projectRepository.loadAll();
		for (Project p : projectList) {
			projectTransientRepository.save(p);
		}
	}

	/**
	 * Loads the project with the given name.
	 * 
	 * @param projectName
	 * @return A {@link Project}. Throws a {@link FrameworkException} if the
	 *         project doesn't exist
	 */
	public Project loadProject(String projectName) {
		// we load the project from the transient repository, to load the
		// observers with it
		Project project = projectTransientRepository.load(projectName);
		if (project == null)
			throw new FrameworkException("Ce projet n'existe pas");
		return project;
	}

	/**
	 * Saves the given project in the repository. If a project with the same
	 * name already exists, it will be overwritten
	 * 
	 * @param project
	 */
	public void saveProject(Project project) {
		// we save the project in both repositories, to persist data in the
		// cache and to persist the observers during runtime
		projectRepository.save(project);
		projectTransientRepository.save(project);
	}

	/**
	 * Adds a product to the given project, to a given category
	 * 
	 * @param projectName
	 * @param product
	 * @param category
	 */
	public void addProduct(String projectName, Product product, String category) {
		Project project = loadProject(projectName);
		project.addProduct(product, category);
		saveProject(project);
	}

	/**
	 * Adds an ensemble of product to the given project, to a given category
	 * 
	 * @param projectName
	 * @param products
	 * @param category
	 */
	public void addProducts(String projectName, List<Product> products,
			String category) {
		Project project = loadProject(projectName);
		project.addProducts(products, category);
		saveProject(project);
	}

	/**
	 * Removes a product from a category of a project
	 * 
	 * @param projectName
	 * @param product
	 * @param category
	 */
	public void removeProduct(String projectName, Product product,
			String category) {
		Project project = loadProject(projectName);
		project.removeProduct(product, category);
		saveProject(project);
	}

	/**
	 * Adds an existing category to a project
	 * 
	 * @param projectName
	 * @param cart
	 */
	public void addCategory(String projectName, Cart cart) {
		Project project = loadProject(projectName);
		project.addCategory(cart);
		saveProject(project);
	}

	/**
	 * Adds a new category to a project
	 * 
	 * @param projectName
	 * @param category
	 * @param cartBudget
	 */

	public void addCategory(String projectName, String category,
			double cartBudget) {
		Project project = loadProject(projectName);
		project.addCategory(category, cartBudget);
		saveProject(project);

	}

	/**
	 * Removes a category from a project
	 * 
	 * @param projectName
	 * @param category
	 */
	public void removeCategory(String projectName, String category) {
		Project project = loadProject(projectName);
		project.removeCategory(category);
		saveProject(project);
	}

	/**
	 * 
	 * @param projectName
	 * @param category
	 * @return A cart corresponding to the category
	 */
	public Cart getCategory(String projectName, String category) {
		Project project = loadProject(projectName);
		return project.getCategory(category);

	}

	/**
	 * 
	 * @param projectName
	 * @return The carts of a project
	 */
	public Map<String, Cart> getCarts(String projectName) {
		Project project = loadProject(projectName);
		return project.getCarts();
	}

	/**
	 * Sets a project's budget
	 * 
	 * @param projectName
	 * @param budget
	 */
	public void setBudget(String projectName, double budget) {
		Project project = loadProject(projectName);
		project.setBudget(budget);
		saveProject(project);
	}

	/**
	 * Creates a new empty project with the given name and saves it in the
	 * repository. If a project with the same name already exists, a
	 * {@link FrameworkException} will be thrown
	 * 
	 * @param name
	 */
	public void newProject(String name) {
		Project project = ProjectFactory.newProject(name);
		newProject(project);
	}

	/**
	 * Creates a {@link Project} with the given budget and saves it in the
	 * repository. If a project with the same name already exists, a
	 * {@link FrameworkException} will be thrown
	 * 
	 * @param name
	 * @param budget
	 * @return the new {@link Project}
	 */
	public Project newProject(String name, double budget) {
		Project project = ProjectFactory.newProject(name, budget);
		newProject(project);
		return project;
	}

	/**
	 * Saves the new project in the repository. If a project with the same name
	 * already exists, a {@link FrameworkException} will be thrown
	 * 
	 * @param project
	 * 
	 * @return the new {@link Project}
	 */
	public Project newProject(Project project) {

		if (projectRepository.load(project.getId()) != null)
			throw new FrameworkException("Un projet avec ce nom existe déjà");
		saveProject(project);

		return project;
	}

	/**
	 * Asynchronous method returning a {@link List} of {@link Offer}s which are
	 * the best Offers according to the user's preferences
	 * 
	 * @param projectName
	 *            The name of the project
	 * @param cartName
	 *            The cart
	 * @param callback
	 *            The {@link Callback} to keep this method asynchronous
	 */
	public void getBestOffers(String projectName, String cartName,
			Callback<List<Product>> callback) {
		Cart cart = getCategory(projectName, cartName);

		final int cartSize = cart.getSize();

		ChooseOffersCallback chooseOffersCallback = new ChooseOffersCallback(
				cart.getOffers(), cartSize, callback);

		UserPreferences userPreference = preferencesService
				.getUserPreferences();
		for (Article article : cart.getArticles()) {
			productFinder.getBestOffer(article, userPreference,
					chooseOffersCallback);
		}

		for (Model model : cart.getModels()) {
			productFinder.getBestOffer(model, userPreference,
					chooseOffersCallback);

		}

	}

	/**
	 * Deletes the project with the given id if it exists
	 * 
	 * @param id
	 */
	public void deleteProject(String id) {
		projectRepository.remove(id);
	}

	/**
	 * 
	 * @return All the projects stored in the repository
	 */
	public List<Project> getAllProjects() {
		return projectRepository.loadAll();
	}

	/**
	 * Gets the budget of a project. Throws a {@link FrameworkException} if the
	 * project does not exist
	 * 
	 * @param projectName
	 * @return the budget of the project
	 */
	public double getBudget(String projectName) {
		Project project = loadProject(projectName);
		return project.getBudget();
	}

	/**
	 * Gets the budget of a project's category. Throws a
	 * {@link FrameworkException} if the project or cart do not exist
	 * 
	 * @param projectName
	 * @param category
	 * @return the budget of the category
	 */
	public double getBudget(String projectName, String category) {
		Cart cart = getCategory(projectName, category);
		return cart.getBudget();
	}

	/**
	 * Removes all products from a category of a project. Throws a
	 * {@link FrameworkException} if the project or cart do not exist
	 * 
	 * @param projectName
	 * @param category
	 */
	public void clearCategory(String projectName, String category) {
		Project project = loadProject(projectName);
		project.getCategory(category).clearProducts();
		saveProject(project);

	}

	/**
	 * Sets the quantity of the a product in a project's category
	 * 
	 * @param projectName
	 * @param category
	 * @param product
	 * @param quantity
	 */
	public void setQuantity(String projectName, String category,
			Product product, int quantity) {
		Project project = loadProject(projectName);
		project.getCategory(category).setQuantity(product, quantity);
		saveProject(project);

	}
}
